// Copyright (c) 2010 Dan Popick

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

grammar Css3;

/*
 * Adapted from a CSS3 grammar found on the ANTLR website:
 * http://www.antlr.org/grammar/list
 *
 * trihus Tue Jul 1, 2008 13:43
 * This CSS parser handles valid CSS syntax. It will also accept property values not defined by CSS without
 * giving an error. It has been debugged with ANTLRWorks and tested on a number of CSS files. (You must comment
 * out language=CSharp for it to work with ANTLRWorks). I don't think it handles the full syntax of @import or @include.
 *
 */
  
options {
	language='CSharp2';
	output=AST;
//	ASTLabelType=CommonTree;
//	k=4;
}

tokens {
	ATTRIBEQUAL;
	HASVALUE;
	BEGINSWITH;
}

@parser::namespace { Open.Web.Stijl.Parsing }
@lexer::namespace { Open.Web.Stijl.Parsing }

@header {
  using System.Text;
  using System.Collections.Generic;
  using Open.Web.Stijl.Css;
}

@members {
  public List<object> Components { get; set; }
}

stylesheet
@init {
  Components = new List<object>();
}
	// charset and namespace not supported
	: 
	( 
		importRule
	)*
	(
		mg=mediaGroup | { if (mg != null) Components.Add($mg.group); }
		ns=nested | { if (ns != null) Components.Add($ns.rule); }
		rs=ruleset { if (rs != null) Components.Add($rs.rule); }
	)+ 
	;

importRule returns[Function importUrl]
	: ('@import' | '@include')  function ';'? 
	{
		if (String.Equals($function.function.Name, "url", StringComparison.InvariantCultureIgnoreCase))
			Components.Add(new Import($function.function.Arguments));
		else
			Components.Add($importRule.text);
	}
	;

mediaGroup returns[MediaGroup group]
	: '@media' identList '{' ruleSetGroup '}' { $group = new MediaGroup($identList.text, $ruleSetGroup.rules); }
	;
	
identList returns[List<string> idents]
@init {
	$idents = new List<string>();
}
	: i1=IDENT { $idents.Add($i1.text); }
	  (
	  	',' 
	  	i2=IDENT { if (i2 != null) $idents.Add($i2.text); }
	  )*
	;
	
ruleSetGroup returns[List<RuleSet> rules]
@init {
	$rules = new List<RuleSet>();
}
	: (
		ruleset { $rules.Add($ruleset.rule); }
	  )*
	;

nested returns[RuleSet rule]
 	: '@' nest '{' properties? nested* '}' 
 	{
 		RuleSet ruleSet = RuleSetFactory.Create("@" + $nest.text, $properties.properties);
 		Components.Add(ruleSet);
 		$rule = ruleSet;
 	}
	;
nest
	: IDENT IDENT* pseudo* 
	;

pseudo
	: (':'|'::') IDENT 
	| (':'|'::') function 
	;

ruleset returns[RuleSet rule]
 	: selectors '{' properties? '}' 
 	{ 
 		RuleSet ruleSet = RuleSetFactory.Create($selectors.selectorSet, $properties.properties); 
 		$rule = ruleSet;
 	}
	;
	
selectors returns[string selectorSet]
@init {
	StringBuilder sb = new StringBuilder();
}
@after {
	$selectorSet = sb.ToString();
}
	: s1=selector		{ sb.Append($s1.selectorText); }
	 (
		',' s2=selector	{ sb.AppendFormat(",{0}", $s2.selectorText); }
	 )*
	;
	
selector returns[string selectorText]
@init {
	StringBuilder sb = new StringBuilder();
}
@after {
	$selectorText = sb.ToString();
}
	: (i=IDENT { sb.Append($i.text); }
		(
			s1=('>' | '+' | ':')?		{ if (!String.IsNullOrEmpty($s1.text)) sb.Append($s1.text); else sb.Append(' '); }
	  		(
	  			i2=IDENT	{ sb.Append($i2.text); }
	  			| a1=attrib 	{ sb.Append($a1.text); }
	  			| f2=function	{ sb.Append($f2.text); }
	  		)	
	  	)*
	  |
		s3=attrib				{ if (!String.IsNullOrEmpty($s3.text)) sb.Append($s3.text); }
	) 
	;
	
properties returns[List<Declaration> properties]
@init {
	$properties = new List<Declaration>();
}
	: d1=declaration 
		{ $properties.Add($d1.declaration); }
	 (';' d2=declaration
	 	{ if (d2 != null) $properties.Add($d2.declaration); }
	 )*
	 ';'?
	;
	
attrib
	: '[' IDENT (attribRelate (STRING | IDENT))? ']'
	;

attribRelate
	: '='  -> ATTRIBEQUAL
	| '~=' -> HASVALUE
	| '|=' -> BEGINSWITH
	;	
  
declaration returns[Declaration declaration]
	: IDENT ':' args { $declaration = new Declaration($IDENT.text, $args.argList); }
	;

args returns[List<object> argList]
@init {
	$argList = new List<object>();
}
	: e1=expr { $argList.Add($e1.argument); }
	(
		','? e2=expr { $argList.Add($e2.argument); }
	)*
	;

expr returns[object argument]
	: IDENT { $argument = $IDENT.text; }
	| STRING { $argument = $STRING.text; }
	| function { $argument = $function.function; }
	;

function returns[Function function]
	: 
	IDENT '(' args? ')' { $function = new Function($IDENT.text, $args.text); }
	;


/* 
 * For the sake of simplicity, 	we match '#' as any part of IDENT (including 1st char)
 * in order to ignore the difference between tag, tag#id, &c., selectors since we don't care
 * in this context.
 *
 * Also, for whatever reason, the 'selector' production was having trouble with identity
 * selectors which start like colors (e.g., #cab). Not really sure why, but since we don't care 
 * about the syntactic difference between colors and other values, I removed it.
 */
IDENT
	:	('_' | '0' .. '9' | 'a'..'z'| 'A'..'Z' | '\u0100'..'\ufffe' | '#' | '.' | '/' | '%' | '!') 
		('_' | '-' | '0' .. '9' | 'a'..'z'| 'A'..'Z' | '\u0100'..'\ufffe' | '0'..'9' | '#' | '.' | '/' | '%' | '!')*
	|	'-' ('_' | '0' .. '9' | 'a'..'z'| 'A'..'Z' | '\u0100'..'\ufffe' | '#' | '.' | '/' | '%' | '!') 
		('_' | '-' | '0' .. '9' | 'a'..'z'| 'A'..'Z' | '\u0100'..'\ufffe' | '0'..'9' | '#' | '.' | '/' | '%' | '!')*
	;


// Single-line comments
STRING 
	:	'"' (~('"'|'\n'|'\r')|'\\\"')* '"'
	|	'\'' (~('\''|'\n'|'\r')|'\\\'')* '\''
	;

// Whitespace -- ignored
WS	: ( ' ' | '\t' | '\r' | '\n' | '\f' )+ { $channel = HIDDEN; }
	;

SL_COMMENT
	:	'//'
		(~('\n'|'\r'))* ('\n'|'\r'('\n')?)
		{$channel=HIDDEN;}
	;
	
// multiple-line comments
COMMENT
	:	'/*' .* '*/' { $channel = HIDDEN; }
	;
